﻿/* ----------------------------------------------------------------------------
 * Copyright (c) Philip Stears 
 *
 * This source code is subject to terms and conditions of the Microsoft Public 
 * License (Ms-PL).
 * 
 * By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 * 
 * To request a copy of the Microsoft Public License, send a request by email
 * to license@connectbasic.net.
 *
 * This notice must not be removed from this software.
 * ---------------------------------------------------------------------------- */

namespace ConnectBasic.ObjectModel
{
    using System;
    using ConnectBasic.Execution;
    using ConnectBasic.Execution.Binding;
    using DlrExpression = Microsoft.Linq.Expressions.Expression;

    internal static class GeneratorUtil
    {

        // Used to drive numeric type coercion
        private enum NumCode
        {
            Invalid = -1,
            Byte,
            Integer,
            Long,
            Single,
            Double,
            Decimal
        }

        private static readonly NumCode[,] NumericTypes =
        {
            //              Byte,               Integer,            Long,               Single,             Double,         Decimal
            /* Byte     */ {NumCode.Byte,       NumCode.Integer,    NumCode.Long,       NumCode.Single,     NumCode.Double,     NumCode.Decimal},
            /* Integer  */ {NumCode.Integer,    NumCode.Integer,    NumCode.Long,       NumCode.Single,     NumCode.Double,     NumCode.Decimal},
            /* Long     */ {NumCode.Long,       NumCode.Long,       NumCode.Long,       NumCode.Single,     NumCode.Double,     NumCode.Decimal},
            /* Single   */ {NumCode.Single,     NumCode.Single,     NumCode.Single,     NumCode.Single,     NumCode.Double,     NumCode.Decimal},
            /* Double   */ {NumCode.Double,     NumCode.Double,     NumCode.Double,     NumCode.Double,     NumCode.Double,     NumCode.Decimal},
            /* Double   */ {NumCode.Decimal,    NumCode.Decimal,    NumCode.Decimal,    NumCode.Decimal,    NumCode.Decimal,    NumCode.Decimal}
        };

        /// <summary>
        /// Wraps the given expression in an appropriate cast/convert to the given type.
        /// </summary>
        /// <param name="expression">The expression to cast/convert</param>
        /// <param name="desiredType">The type to which to convert.</param>
        /// <returns></returns>
        public static DlrExpression Convert(DlrExpression expression, Type desiredType)
        {
            if (desiredType.IsAssignableFrom(expression.Type))
            {
                return expression;
            }
            else
            {
                return DlrExpression.Dynamic(new BasicConvertBinder(desiredType), desiredType, expression);
            }
        }

        public static DlrExpression GenerateBinaryOperation(BinaryOperationKind kind, DlrExpression left, DlrExpression right)
        {

            // TODO: Detect operations which don't need to be dynamic!
            switch (kind)
            {

                case BinaryOperationKind.Add:
                case BinaryOperationKind.Subtract:
                case BinaryOperationKind.Multiply:
                case BinaryOperationKind.DivideFloating:
                case BinaryOperationKind.Power:

                    if (CooerceNumeric(ref left, ref right))
                    {

                        // Emit a static operation
                        switch (kind)
                        {
                            case BinaryOperationKind.Add:
                                return DlrExpression.AddChecked(left, right);

                            case BinaryOperationKind.Subtract:
                                return DlrExpression.SubtractChecked(left, right);

                            case BinaryOperationKind.Multiply:
                                return DlrExpression.MultiplyChecked(left, right);

                            case BinaryOperationKind.DivideFloating:
                                return DlrExpression.Divide(left, right);

                            case BinaryOperationKind.Power:
                                return DlrExpression.Power(left, right);

                            default:
                                throw new InvalidProgramException();
                        }                        
                    }
                    else
                    {

                        // Emit a dynamic operation
                        return DlrExpression.Dynamic(new BasicBinaryOperationBinder(kind), typeof(object), left, right);
                    }

                case BinaryOperationKind.Concatenate:
                    return DlrExpression.Dynamic(new BasicBinaryOperationBinder(kind), typeof(string), left, right);

                case BinaryOperationKind.EqualTo:
                case BinaryOperationKind.GreaterThan:
                case BinaryOperationKind.GreaterThanOrEqualTo:
                case BinaryOperationKind.LessThan:
                case BinaryOperationKind.LessThanOrEqualTo:
                case BinaryOperationKind.NotEqualTo:
                    return DlrExpression.Dynamic(new BasicBinaryOperationBinder(kind), typeof(bool), left, right);

                case BinaryOperationKind.RefIsEqual:
                    return DlrExpression.ReferenceEqual(left, right);

                case BinaryOperationKind.RefIsNotEqual:
                    return DlrExpression.Not(DlrExpression.ReferenceEqual(left, right));

                default:
                    throw new Exception("Unsupported operator!");
            }
        }

        public static DlrExpression GenerateBinaryOperation(BasicCodeGenerator gen, BinaryOperationKind kind, ExpressionBase abstractLeft, ExpressionBase abstractRight)
        {
            var left = abstractLeft.Compile(gen);
            var right = abstractRight.Compile(gen);

            return GenerateBinaryOperation(kind, left, right);
        }

        private static bool CooerceNumeric(ref DlrExpression left, ref DlrExpression right)
        {
            var leftType = TypeToNumCode(left.Type);
            var rightType = TypeToNumCode(right.Type);

            // Make sure they're both numeric
            if (leftType == NumCode.Invalid || rightType == NumCode.Invalid)
            {
                return false;
            }

            // If they're the same size, we're okay
            if (leftType == rightType)
            {
                return true;
            }

            // Lookup the type we need to convert them both to
            var targetType = NumericTypes[(int)leftType, (int)rightType];

            if (leftType != targetType)
            {
                left = EmitConversion(left, targetType);
            }

            if (rightType != targetType)
            {
                right = EmitConversion(right, targetType);
            }

            return true;
        }

        private static DlrExpression EmitConversion(DlrExpression source, NumCode targetTypeCode)
        {
            Type targetType = NumCodeToType(targetTypeCode);

            if (source.Type != targetType)
            {
                source = DlrExpression.ConvertChecked(source, targetType);
            }

            return source;
        }

        private static Type NumCodeToType(NumCode code)
        {
            switch (code)
            {
                case NumCode.Byte:
                    return typeof(SByte);

                case NumCode.Integer:
                    return typeof(short);

                case NumCode.Long:
                    return typeof(int);

                case NumCode.Single:
                    return typeof(float);

                case NumCode.Double:
                    return typeof(double);

                case NumCode.Decimal:
                    return typeof(decimal);

                default:
                    throw new ArgumentOutOfRangeException("code");
            }
        }

        private static NumCode TypeToNumCode(Type type)
        {
            if (type == typeof(sbyte))
            {
                return NumCode.Byte;
            }
            else if (type == typeof(short))
            {
                return NumCode.Integer;
            }
            else if (type == typeof(int))
            {
                return NumCode.Long;
            }
            else if (type == typeof(float))
            {
                return NumCode.Single;
            }
            else if (type == typeof(double))
            {
                return NumCode.Double;
            }
            else if (type == typeof(decimal))
            {
                return NumCode.Decimal;
            }
            else
            {
                return NumCode.Invalid;
            }
        }
    }
}
